package com.example.shortlink.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.example.shortlink.common.convention.exception.ClientException;
import com.example.shortlink.common.convention.exception.ServiceException;
import com.example.shortlink.dao.entity.*;
import com.example.shortlink.dao.mapper.*;
import com.example.shortlink.dto.biz.ShortLinkStatsRecordDTO;
import com.example.shortlink.dto.req.ShortLinkBatchCreateReqDTO;
import com.example.shortlink.dto.req.ShortLinkCreateReqDTO;
import com.example.shortlink.dto.req.ShortLinkPageReqDTO;
import com.example.shortlink.dto.req.ShortLinkUpdateReqDTO;
import com.example.shortlink.dto.resp.*;
import com.example.shortlink.mq.producer.DelayShortLinkStatsProducer;
import com.example.shortlink.service.ShortLinkService;
import com.example.shortlink.toolkit.HashUtil;
import com.example.shortlink.toolkit.LinkUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryChain;
import com.mybatisflex.core.query.QueryMethods;
import com.mybatisflex.core.query.QueryOrderBy;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.example.shortlink.common.constant.RedisKeyConstant.*;
import static com.example.shortlink.common.constant.ShortLinkConstant.AMAP_REMOTE_URL;
import static com.example.shortlink.common.enums.VailDateTypeEnum.PERMANENT;
import static com.example.shortlink.dao.entity.table.LinkStatsTodayTableDef.LINK_STATS_TODAY;
import static com.example.shortlink.dao.entity.table.ShortLinkGotoTableDef.SHORT_LINK_GOTO;
import static com.example.shortlink.dao.entity.table.ShortLinkTableDef.SHORT_LINK;
import static com.mybatisflex.core.query.QueryMethods.case_;

@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLink> implements ShortLinkService {
    private final RedissonClient redissonClient;
    private final StringRedisTemplate stringRedisTemplate;
    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;
    private final DelayShortLinkStatsProducer delayShortLinkStatsProducer;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final LinkStatsTodayMapper linkStatsTodayMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;


    @Value("${short-link.stats.locale.amap-key}")
    private String statsLocaleAmapKey;

    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;

    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
        // 获取请求的服务器的名称
        String serverName = request.getServerName();
        // 查询中间代理表查询端口
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> StrUtil.COLON + each)
                .orElse(StrUtil.EMPTY);
        // 拼接完整的短链接
        String fullShortUrl = serverName + serverPort + StrUtil.C_SLASH + shortUri;
        // 查询原始链接
        String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        // 存在原始链接,则跳转原始链接
        if (StrUtil.isNotBlank(originalLink)) {
            ShortLinkStatsRecordDTO statsRecord = this.buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            this.shortLinkStats(fullShortUrl, null, statsRecord);
            ((HttpServletResponse) response).sendRedirect(originalLink);
            return;
        }
        // 布隆过滤器判断请求中 key是否存在
        boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        // 不存在直接返回
        if (!contains) {
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        // 校验请求 key是否存在空值
        String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        // 存在直接返回
        if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        // 此处避免 key查询失效,大量请求落到数据库中,加入分布式锁 - 缓存击穿
        RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        lock.lock();
        try {
            // 再次检查是否存在原始链接
            originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
            // 存在原始链接,则跳转原始链接
            if (StrUtil.isNotBlank(originalLink)) {
                ShortLinkStatsRecordDTO statsRecord = this.buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
                this.shortLinkStats(fullShortUrl, null, statsRecord);
                ((HttpServletResponse) response).sendRedirect(originalLink);
                return;
            }
            // 查询短链接
            ShortLinkGoto shortLinkGoto = QueryChain.of(shortLinkGotoMapper)
                    .where(SHORT_LINK_GOTO.FULL_SHORT_URL.eq(fullShortUrl))
                    .oneOpt()
                    .orElse(null);
            // 不存在短链接,则直接返回
            if (null == shortLinkGoto) {
                // 设置短链接空值和过期时间 30分钟防止缓存穿透
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), StrUtil.DASHED, 30, TimeUnit.MINUTES);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            // 根据短链接查询原始链接
            ShortLink shortLink = QueryChain.of(mapper)
                    .where(SHORT_LINK.GID.eq(shortLinkGoto.getGid())
                            .and(SHORT_LINK.FULL_SHORT_URL.eq(fullShortUrl))
                            .and(SHORT_LINK.DEL_FLAG.eq(Boolean.FALSE))
                            .and(SHORT_LINK.ENABLE_STATUS.eq(Boolean.FALSE)))
                    .oneOpt()
                    .orElse(null);
            if (shortLink == null || (shortLink.getValidDate() != null && shortLink.getValidDate().before(new Date()))) {
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            // 查询到原始链接后放入缓存
            stringRedisTemplate.opsForValue().set(
                    String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    shortLink.getOriginUrl(),
                    LinkUtil.getLinkCacheValidTime(shortLink.getValidDate()), TimeUnit.MILLISECONDS
            );
            ShortLinkStatsRecordDTO statsRecord = this.buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            this.shortLinkStats(fullShortUrl, shortLink.getGid(), statsRecord);
            // 跳转原始链接
            ((HttpServletResponse) response).sendRedirect(shortLink.getOriginUrl());
        } finally {
            lock.unlock();
        }
    }

    /**
     * 短链接统计
     *
     * @param fullShortUrl         完整短链接
     * @param gid                  分组标识
     * @param shortLinkStatsRecord 短链接统计实体参数
     */
    @Override
    public void shortLinkStats(String fullShortUrl, String gid, ShortLinkStatsRecordDTO shortLinkStatsRecord) {
        fullShortUrl = Optional.ofNullable(fullShortUrl).orElse(shortLinkStatsRecord.getFullShortUrl());
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, fullShortUrl));
        RLock rLock = readWriteLock.readLock();
        if (!rLock.tryLock()) {
            delayShortLinkStatsProducer.send(shortLinkStatsRecord);
            return;
        }
        try {
            if (StrUtil.isBlank(gid)) {
                QueryWrapper wrapper = QueryWrapper.create()
                        .where(SHORT_LINK_GOTO.FULL_SHORT_URL.eq(fullShortUrl));
                ShortLinkGoto shortLinkGoto = shortLinkGotoMapper.selectOneByQuery(wrapper);
                gid = shortLinkGoto.getGid();
            }
            // 获取当前小时
            int hour = DateUtil.hour(new Date(), true);
            // 获取当日所在周
            Week week = DateUtil.dayOfWeekEnum(new Date());
            int weekValue = week.getIso8601Value();
            // 短链接基础访问监控实体
            LinkAccessStats linkAccessStats = LinkAccessStats.builder()
                    .pv(1)
                    .uv(shortLinkStatsRecord.getUvFirstFlag() ? 1 : 0)
                    .uip(shortLinkStatsRecord.getUipFirstFlag() ? 1 : 0)
                    .hour(hour)
                    .weekday(weekValue)
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .date(new Date())
                    .build();
            linkAccessStatsMapper.shortLinkStats(linkAccessStats);
            // 使用高德 api查询国家和省份
            Map<String, Object> localeParamMap = new HashMap<>();
            localeParamMap.put("key", statsLocaleAmapKey);
            localeParamMap.put("ip", shortLinkStatsRecord.getRemoteAddr());
            String localeResultStr = HttpUtil.get(AMAP_REMOTE_URL, localeParamMap);
            JSONObject localeResultObj = JSON.parseObject(localeResultStr);
            String infoCode = localeResultObj.getString("infocode");
            String actualProvince = "未知";
            String actualCity = "未知";
            if (StrUtil.isNotBlank(infoCode) && StrUtil.equals(infoCode, "10000")) {
                String province = localeResultObj.getString("province");
                boolean unknownFlag = StrUtil.equals(province, "[]");
                LinkLocaleStats linkLocaleStats = LinkLocaleStats.builder()
                        .province(actualProvince = unknownFlag ? actualProvince : province)
                        .city(actualCity = unknownFlag ? actualCity : localeResultObj.getString("city"))
                        .adcode(unknownFlag ? "未知" : localeResultObj.getString("adcode"))
                        .cnt(1)
                        .fullShortUrl(fullShortUrl)
                        .country("中国")
                        .gid(gid)
                        .date(new Date())
                        .build();
                linkLocaleStatsMapper.shortLinkLocaleState(linkLocaleStats);
            }
            LinkOsStats linkOsStats = LinkOsStats.builder()
                    .os(shortLinkStatsRecord.getOs())
                    .cnt(1)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkOsStatsMapper.shortLinkOsState(linkOsStats);
            LinkBrowserStats linkBrowserStats = LinkBrowserStats.builder()
                    .browser(shortLinkStatsRecord.getBrowser())
                    .cnt(1)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkBrowserStatsMapper.shortLinkBrowserState(linkBrowserStats);
            LinkDeviceStats linkDeviceStats = LinkDeviceStats.builder()
                    .device(shortLinkStatsRecord.getDevice())
                    .cnt(1)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStats);
            LinkNetworkStats linkNetworkStats = LinkNetworkStats.builder()
                    .network(shortLinkStatsRecord.getNetwork())
                    .cnt(1)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStats);
            LinkAccessLogs linkAccessLogs = LinkAccessLogs.builder()
                    .user(shortLinkStatsRecord.getUv())
                    .ip(shortLinkStatsRecord.getRemoteAddr())
                    .browser(shortLinkStatsRecord.getBrowser())
                    .os(shortLinkStatsRecord.getOs())
                    .network(shortLinkStatsRecord.getNetwork())
                    .device(shortLinkStatsRecord.getDevice())
                    .locale(StrUtil.join("-", "中国", actualProvince, actualCity))
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .build();
            linkAccessLogsMapper.insert(linkAccessLogs);
            mapper.incrementStats(gid, fullShortUrl, 1, shortLinkStatsRecord.getUvFirstFlag() ? 1 : 0, shortLinkStatsRecord.getUipFirstFlag() ? 1 : 0);
            LinkStatsToday linkStatsTodayDO = LinkStatsToday.builder()
                    .todayPv(1)
                    .todayUv(shortLinkStatsRecord.getUvFirstFlag() ? 1 : 0)
                    .todayUip(shortLinkStatsRecord.getUipFirstFlag() ? 1 : 0)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkStatsTodayMapper.shortLinkTodayState(linkStatsTodayDO);
        } catch (Throwable ex) {
            log.error("短链接访问量统计异常", ex);
        } finally {
            rLock.unlock();
        }
    }

    /**
     * 创建短链
     *
     * @param requestParam 短链接创建请求对象
     * @return 短链接创建响应对象
     */
    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        // 生成短链接后缀
        String shortLinkSuffix = this.generateSuffix(requestParam);
        // 生成完整短链接
        String fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
                .append("/")
                .append(shortLinkSuffix)
                .toString();
        ShortLink shortLink = ShortLink.builder()
                .domain(createShortLinkDefaultDomain)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .createdType(requestParam.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .describe(requestParam.getDescribe())
                .shortUri(shortLinkSuffix)
                .enableStatus(0)
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .delTime(0L)
                .fullShortUrl(fullShortUrl)
                .favicon(this.getFavicon(requestParam.getOriginUrl()))
                .build();
        ShortLinkGoto linkGoto = ShortLinkGoto.builder()
                .fullShortUrl(fullShortUrl)
                .gid(requestParam.getGid())
                .build();
        try {
            mapper.insert(shortLink);
            shortLinkGotoMapper.insert(linkGoto);
        } catch (DuplicateKeyException ex) {
            QueryChain.of(mapper)
                    .select(SHORT_LINK.DEFAULT_COLUMNS)
                    .from(SHORT_LINK)
                    .where(SHORT_LINK.FULL_SHORT_URL.eq(fullShortUrl))
                    .oneOpt().orElseThrow(() -> {
                        log.warn("短链接：{} 重复入库", fullShortUrl);
                        return new ServiceException("短链接生成重复");
                    });
        }
        stringRedisTemplate.opsForValue().set(
                String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                requestParam.getOriginUrl(),
                LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS
        );
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + linkGoto.getFullShortUrl())
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();
    }

    /**
     * 批量创建短链接
     *
     * @param requestParam 批量创建短链接请求参数
     * @return 批量创建短链接返回参数
     */
    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();
        List<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();
        for (int i = 0; i < originUrls.size(); i++) {
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            try {
                ShortLinkCreateRespDTO shortLink = createShortLink(shortLinkCreateReqDTO);
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(shortLink.getFullShortUrl())
                        .originUrl(shortLink.getOriginUrl())
                        .describe(describes.get(i))
                        .build();
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable ex) {
                log.error("批量创建短链接失败，原始参数：{}", originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();
    }

    /**
     * 修改短链接
     *
     * @param requestParam 修改短链接请求参数
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        QueryWrapper queryWrapper = QueryWrapper.create()
                .select(SHORT_LINK.DEFAULT_COLUMNS)
                .from(SHORT_LINK)
                .where(SHORT_LINK.FULL_SHORT_URL.eq(requestParam.getFullShortUrl())
                        .and(SHORT_LINK.GID.eq(requestParam.getGid()))
                        .and(SHORT_LINK.DEL_FLAG.eq(Boolean.FALSE))
                        .and(SHORT_LINK.ENABLE_STATUS.eq(Boolean.FALSE)));
        ShortLink hasShortLink = mapper.selectOneByQuery(queryWrapper);
        if (null == hasShortLink) {
            log.warn("修改短链接失败，原始参数：{}", requestParam);
            throw new ClientException("短链接记录不存在");
        }
        if (Objects.equals(hasShortLink.getGid(), requestParam.getGid())) {
            QueryWrapper updateWrapper = QueryWrapper.create()
                    .where(SHORT_LINK.FULL_SHORT_URL.eq(requestParam.getFullShortUrl())
                            .and(SHORT_LINK.GID.eq(requestParam.getGid()))
                            .and(SHORT_LINK.DEL_FLAG.eq(Boolean.FALSE))
                            .and(SHORT_LINK.ENABLE_STATUS.eq(Boolean.FALSE)));
            ShortLink shortLink = ShortLink.builder()
                    .domain(hasShortLink.getDomain())
                    .shortUri(hasShortLink.getShortUri())
                    .favicon(hasShortLink.getFavicon())
                    .createdType(hasShortLink.getCreatedType())
                    .gid(requestParam.getGid())
                    .originUrl(requestParam.getOriginUrl())
                    .describe(requestParam.getDescribe())
                    .validDateType(Objects.equals(requestParam.getValidDateType(), PERMANENT.getType()) ? requestParam.getValidDateType() : null)
                    .validDate(requestParam.getValidDate())
                    .build();
            mapper.updateByQuery(shortLink, updateWrapper);
        } else {
            QueryWrapper linkUpdateWrapper = QueryWrapper.create()
                    .where(SHORT_LINK.FULL_SHORT_URL.eq(requestParam.getFullShortUrl())
                            .and(SHORT_LINK.GID.eq(hasShortLink.getGid()))
                            .and(SHORT_LINK.DEL_TIME.eq(0L))
                            .and(SHORT_LINK.DEL_FLAG.eq(Boolean.FALSE))
                            .and(SHORT_LINK.ENABLE_STATUS.eq(Boolean.FALSE)));
            ShortLink delShortLink = ShortLink.builder()
                    .delTime(System.currentTimeMillis())
                    .build();
            delShortLink.setDelFlag(Boolean.TRUE);
            mapper.updateByQuery(delShortLink, linkUpdateWrapper);
            ShortLink shortLink = ShortLink.builder()
                    .domain(createShortLinkDefaultDomain)
                    .originUrl(requestParam.getOriginUrl())
                    .gid(requestParam.getGid())
                    .createdType(hasShortLink.getCreatedType())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .describe(requestParam.getDescribe())
                    .shortUri(hasShortLink.getShortUri())
                    .enableStatus(hasShortLink.getEnableStatus())
                    .totalPv(hasShortLink.getTotalPv())
                    .totalUv(hasShortLink.getTotalUv())
                    .totalUip(hasShortLink.getTotalUip())
                    .fullShortUrl(hasShortLink.getFullShortUrl())
                    .favicon(getFavicon(requestParam.getOriginUrl()))
                    .delTime(0L)
                    .build();
            mapper.insert(shortLink);
        }

    }

    /**
     * 分页查询短链接
     *
     * @param requestParam 分页查询短链接请求参数
     * @return 短链接分页返回结果
     */
    @Override
    public Page<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        QueryOrderBy orderByConditions = this.pageOrderByConditions(requestParam);
        QueryWrapper wrapper = QueryWrapper.create()
                .select(SHORT_LINK.DEFAULT_COLUMNS,
                        case_().when(LINK_STATS_TODAY.TODAY_PV.isNotNull())
                                .then(LINK_STATS_TODAY.TODAY_PV)
                                .else_(0)
                                .end().as("today_pv"),
                        case_().when(LINK_STATS_TODAY.TODAY_UV.isNotNull())
                                .then(LINK_STATS_TODAY.TODAY_UV)
                                .else_(0)
                                .end().as("today_uv"),
                        case_().when(LINK_STATS_TODAY.TODAY_UIP.isNotNull())
                                .then(LINK_STATS_TODAY.TODAY_UIP)
                                .else_(0)
                                .end().as("today_uip"))
                .from(SHORT_LINK.as("t"))
                .leftJoin(LINK_STATS_TODAY).as("s")
                .on(SHORT_LINK.GID.eq(LINK_STATS_TODAY.GID)
                        .and(SHORT_LINK.FULL_SHORT_URL.eq(LINK_STATS_TODAY.FULL_SHORT_URL))
                        .and(LINK_STATS_TODAY.DATE.eq(LocalDateTime.now())))
                .where(SHORT_LINK.GID.eq(requestParam.getGid())
                        .and(SHORT_LINK.ENABLE_STATUS.eq(Boolean.FALSE))
                        .and(SHORT_LINK.DEL_FLAG.eq(Boolean.FALSE)))
                .orderBy(orderByConditions);
        Page<ShortLink> resultPage = mapper.paginate(requestParam, wrapper);
        return resultPage.map(each -> {
            ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            result.setDomain("http://" + result.getDomain());
            return result;
        });
    }

    /**
     * 查询短链接分组内数量
     *
     * @param requestParam 查询短链接分组内数量请求参数
     * @return 查询短链接分组内数量响应
     */
    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        QueryWrapper queryWrapper = QueryWrapper.create()
                .select(SHORT_LINK.GID.as("gid"),
                        QueryMethods.count(SHORT_LINK.ALL_COLUMNS).as("shortLinkCount"))
                .from(SHORT_LINK)
                .where(SHORT_LINK.GID.in(requestParam)
                        .and(SHORT_LINK.ENABLE_STATUS.eq(Boolean.FALSE))
                        .and(SHORT_LINK.DEL_FLAG.eq(Boolean.FALSE))
                        .and(SHORT_LINK.DEL_TIME.eq(0L)))
                .groupBy(SHORT_LINK.GID);
        List<ShortLinkGroupCountQueryRespDTO> shortLinkDOList = mapper.selectListByQueryAs(queryWrapper, ShortLinkGroupCountQueryRespDTO.class);
        return shortLinkDOList.isEmpty() ? Collections.emptyList() : shortLinkDOList;
    }

    /**
     * 分页排序条件
     *
     * @param requestParam 分页查询请求参数
     * @return 排序条件
     */
    private QueryOrderBy pageOrderByConditions(ShortLinkPageReqDTO requestParam) {
        return switch (requestParam.getOrderTag()) {
            case "todayPv" -> LINK_STATS_TODAY.TODAY_PV.desc();
            case "todayUv" -> LINK_STATS_TODAY.TODAY_UV.desc();
            case "todayUip" -> LINK_STATS_TODAY.TODAY_UIP.desc();
            case "totalPv" -> SHORT_LINK.TOTAL_PV.desc();
            case "totalUv" -> SHORT_LINK.TOTAL_UV.desc();
            case "totalUip" -> SHORT_LINK.TOTAL_UIP.desc();
            default -> SHORT_LINK.CREATE_TIME.desc();
        };
    }

    /**
     * 通用短链接后缀生成
     * 1、如果当前的短链接是没用过的，但是被误判用过了，那也没事，直接会被跳过，继续生成
     * 2、如果当前的短链接是用过的，但是被误判说没有用过，那么在插入到数据库的时候，就会被数据库的唯一字段，短链接拦截，出现异常
     * 3、如果在数据库没有拦截，说明没有误判, 获取到的短链就是没用过的，正常进行，不抛异常
     *
     * @param requestParam 短链接创建请求对象
     * @return 短链接后缀
     */
    private String generateSuffix(ShortLinkCreateReqDTO requestParam) {
        int customGenerateCount = 0;
        String shorUri;
        while (true) {
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            String originUrl = requestParam.getOriginUrl();
            originUrl += System.currentTimeMillis();
            shorUri = HashUtil.hashToBase62(originUrl);
            if (!shortUriCreateCachePenetrationBloomFilter.contains(requestParam.getDomain() + "/" + shorUri)) {
                break;
            }
            customGenerateCount++;
        }
        return shorUri;
    }

    /**
     * 获取网站图标
     *
     * @param url 网址
     * @return 图标地址
     */
    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod(HttpMethod.GET.name());
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (HttpURLConnection.HTTP_OK == responseCode) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }

    /**
     * 构建链接统计记录
     * pv 访问次数
     * uv 访问人数(单个用户访问多次相同短链接,该短链接记录用户数为 1次)
     * uip IP数
     *
     * @param fullShortUrl 完整短链接
     * @param request      请求
     * @param response     响应
     * @return 短链接统计实体
     */
    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortUrl, ServletRequest request, ServletResponse response) {
        // 使用 AtomicBoolean保证线程安全
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        // 设置访问cookie保证访问人数不重复增加(同一用户访问人数不增加)
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        AtomicReference<String> uv = new AtomicReference<>();
        Runnable addResponseCookieTask = () -> {
            uv.set(UUID.randomUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            ((HttpServletResponse) response).addCookie(uvCookie);
            uvFirstFlag.set(Boolean.TRUE);
            stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, uv.get());
        };
        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies).filter(each -> Objects.equals(each.getName(), "uv")).findFirst().map(Cookie::getValue).ifPresentOrElse(each -> {
                uv.set(each);
                Long uvAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, each);
                uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
            }, addResponseCookieTask);
        } else {
            addResponseCookieTask.run();
        }
        // 获取客户端真实 IP
        String remoteAddr = LinkUtil.getActualIp(((HttpServletRequest) request));
        // 获取客户端操作系统
        String os = LinkUtil.getOs(((HttpServletRequest) request));
        // 获取客户端浏览器类型
        String browser = LinkUtil.getBrowser(((HttpServletRequest) request));
        // 获取用户访问设备
        String device = LinkUtil.getDevice(((HttpServletRequest) request));
        // 获取用户访问网络
        String network = LinkUtil.getNetwork(((HttpServletRequest) request));
        Long uipAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uip:" + fullShortUrl, remoteAddr);
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .build();
    }
}
